<?php

/**
 * This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT License
 */

/**
 * CSV parser. Converts data between associative array and CSV formats.
 * CSV parsing code borrowed from php-csv-utils by Luke Visinoni
 * http://code.google.com/p/php-csv-utils/
 *
 * @author     Francois Zaninotto
 * @package    propel.runtime.parser
 */
class PropelCSVParser extends PropelParser
{
	const QUOTE_NONE = 0;
	const QUOTE_ALL = 1;
	const QUOTE_NONNUMERIC = 2;
	const QUOTE_MINIMAL = 3;
	
	// these settings are predefined for Excel CSV format
	
	public $delimiter = ',';
	public $lineTerminator = "\r\n";
	public $quotechar = '"';
	public $escapechar = "\\";
	public $quoting = self::QUOTE_MINIMAL;
	
	/**
	 * Converts data from an associative array to CSV.
	 *
	 * @param  array   $array Source data to convert
	 * @param  boolean $isList Whether the input data contains more than one row
	 * @param  boolean $includeHeading Whether the output should contain a heading line
	 *
	 * @return string  Converted data, as a CSV string
	 */
	public function fromArray($array, $isList = false, $includeHeading = true)
	{
		$rows = array();
		if ($isList) {
			if ($includeHeading) {
				$rows[] = implode($this->formatRow(array_keys(reset($array))), $this->delimiter);
			}
			foreach ($array as $row) {
				$rows[] = implode($this->formatRow($row), $this->delimiter);
			}
		} else {
			if ($includeHeading) {
				$rows[] = implode($this->formatRow(array_keys($array)), $this->delimiter);
			}
			$rows[] = implode($this->formatRow($array), $this->delimiter);
		}
		
		return implode($rows, $this->lineTerminator) . $this->lineTerminator;
	}
	
	public function listFromArray($array)
	{
		return $this->fromArray($array, true);
	}
	
	/**
	 * Accepts a row of data and returns it formatted
	 * 
	 * @param array An array of data to be formatted for output to the file
	 * @return array The formatted array
	 */
	protected function formatRow($row)
	{
		foreach ($row as &$column) {
			if (!is_scalar($column)) {
				$column = $this->serialize($column);
			}
			switch ($this->quoting) {
				case self::QUOTE_NONE:
					// do nothing... no quoting is happening here
					break;
				case self::QUOTE_ALL:
					$column = $this->quote($this->escape($column));
					break;
				case self::QUOTE_NONNUMERIC:
					if (preg_match("/[^0-9]/", $column)) {
						$column = $this->quote($this->escape($column));
					}
					break;
				case self::QUOTE_MINIMAL:
				default:
					if ($this->containsSpecialChars($column)) {
						$column = $this->quote($this->escape($column));
					}
					break;
			}
		}
		return $row;
	}

	/**
	* Escapes a column (escapes quotechar with escapechar)
	*
	* @param string A single value to be escaped for output
	* @return string Escaped input value
	*/
	protected function escape($input)
	{
		return str_replace(
			$this->quotechar,
			$this->escapechar . $this->quotechar,
			$input
		);
	}

	/**
	 * Quotes a column with quotechar
	 *
	 * @param string A single value to be quoted for output
	 * @return string Quoted input value
	 */
	protected function quote($input)
	{
		return $this->quotechar . $input . $this->quotechar;
	}

	/**
	 * Returns true if input contains quotechar, delimiter or any of the characters in lineTerminator
	 *
	 * @param string A single value to be checked for special characters
	 * @return boolean True if contains any special characters
	 */
	protected function containsSpecialChars($input)
	{
		$special_chars = str_split($this->lineTerminator, 1);
		$special_chars[] = $this->quotechar;
		$special_chars[] = $this->delimiter;
		foreach ($special_chars as $char) {
			if (strpos($input, $char) !== false) {
				return true;
			}
		}
	}

	/**
	 * Serializes a value to place it into a CSV output
	 */
	protected function serialize($input)
	{
		return serialize($input);
	}
	
	/**
	 * Alias for PropelCSVParser::fromArray()
	 *
	 * @param  array   $array Source data to convert
	 * @param  boolean $isList Whether the input data contains more than one row
	 * @param  boolean $includeHeading Whether the output should contain a heading line
	 *
	 * @return string  Converted data, as a CSV string
	 */
	public function toCSV($array, $isList = false, $includeHeading = true)
	{
		return $this->fromArray($array, $isList, $includeHeading);
	}

	/**
	 * Converts data from CSV to an associative array.
	 *
	 * @param  string  $data Source data to convert, as a CSV string
	 * @param  boolean $isList Whether the input data contains more than one row
	 * @param  boolean $includeHeading Whether the input contains a heading line
	 *
	 * @return array   Converted data
	 */
	public function toArray($data, $isList = false, $includeHeading = true)
	{
		$rows = explode($this->lineTerminator, $data);
		if ($includeHeading) {
			$heading = array_shift($rows);
			$keys = explode($this->delimiter, $heading);
		} else {
			$keys = range(0, count($this->getColumns($rows[0])) - 1);
		}
		if ($isList) {
			$array = array();
			foreach ($rows as $row) {
				$values = $this->cleanupRow($this->getColumns($row));
				if ($values !== array()) {
					$array []= array_combine($keys, $values);
				}
			}
		} else {
			$values = $this->cleanupRow($this->getColumns(array_shift($rows)));
			if ($keys === array('') && $values === array()) {
				$array = array();
			} else {
				if (count($keys) > count($values)) {
					// empty values at the end of the row are not match bu the getColumns() regexp
					$values = array_pad($values, count($keys), null);
				}
				$array = array_combine($keys, $values);
			}
		}
		
		return $array;
	}

	public function listToArray($array)
	{
		return $this->toArray($array, true);
	}

	protected function getColumns($row)
	{
		$delim = preg_quote($this->delimiter, '/');
		preg_match_all('/(".+?"|[^' . $delim . ']+)(' . $delim . '|$)/', $row, $matches);
		return $matches[1];
	}
	
	/**
	 * Accepts a formatted row of data and returns it raw
	 * 
	 * @param array An array of data from a CSV output
	 * @return array The cleaned up array
	 */
	protected function cleanupRow($row)
	{
		foreach ($row as $key => $column) {
			if ($this->isQuoted($column)) {
				$column = $this->unescape($this->unquote($column));
			}
			if ($this->isSerialized($column)) {
				$column = $this->unserialize($column);
			}
			if ($column === 'N;') {
				$column = null;
			}
			$row[$key] = $column;
		}
		return $row;
	}
	
	protected function isQuoted($input)
	{
		$quote = preg_quote($this->quotechar, '/');
		return preg_match('/^' . $quote . '.*' . $quote . '$/', $input);
	}
	
	protected function unescape($input)
	{
		return str_replace(
			$this->escapechar . $this->quotechar,
			$this->quotechar,
			$input
		);
	}
	
	protected function unquote($input)
	{
		return trim($input, $this->quotechar);
	}
	
	/**
	 * Checks whether a value from CSV output is serialized
	 */
	protected function isSerialized($input)
	{
		return preg_match('/^\w\:\d+\:\{/', $input);
	}

	/**
	 * Unserializes a value from CSV output
	 */
	protected function unserialize($input)
	{
		return unserialize($input);
	}
	
	/**
	 * Alias for PropelCSVParser::toArray()
	 *
	 * @param  string  $data Source data to convert, as a CSV string
	 * @param  boolean $isList Whether the input data contains more than one row
	 * @param  boolean $includeHeading Whether the input contains a heading line
	 *
	 * @return array   Converted data
	 */
	public function fromCSV($data, $isList = false, $includeHeading = true)
	{
		return $this->toArray($data, $isList, $includeHeading);
	}
	
}
